﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Data.Linq;
using System.Data.Linq.Mapping;

using SharedGenomics.Core;

namespace SharedGenomics.Domain
{
    [Table(Name = "dbo.FileSchema")]
    [DataContract]
    public class FileSchema
    {
        //private EntitySet<FileSet> _FileSets;
        private EntitySet<FileSchemaColumn> _FileSchemaColumns;
        private KeyedIndexedCollection<string, FileSchemaColumn> _indexedColumns;

        public FileSchema()
        {
            this.Initialize();
        }

        [Column(AutoSync = AutoSync.OnInsert, DbType = "Int NOT NULL IDENTITY", IsPrimaryKey = true, IsDbGenerated = true)]
        [DataMember(Order = 1)]
        public int Id { get; set; }

        [Column(DbType = "Int")]
        [DataMember(Order = 2)]
        public virtual int? GroupId { get; set; }

        [Column(DbType = "VarChar(50)")]
        [DataMember(Order = 3)]
        public string Name { get; set; }

        [DataMember(Order = 4)]
        [Association(Name = "FileSchema_FileSchemaColumn", Storage = "_FileSchemaColumns", ThisKey = "Id", OtherKey = "SchemaId")]
        public EntitySet<FileSchemaColumn> FileSchemaColumns
        {
            get { return this._FileSchemaColumns; }
            set { this._FileSchemaColumns.Assign(value); }
        }

        //public EntitySet<FileSet> FileSets
        //{
        //    get { return this._FileSets; }
        //    set { this._FileSets.Assign(value); }
        //}

        //[Association(Name = "Group_FileSchema", ThisKey = "GroupId", OtherKey = "Id", IsForeignKey = true)]
        //public Group Group { get; set; }

        private void Initialize()
        {
            //create column index to speed up column lookup by name/index
            this._indexedColumns = new KeyedIndexedCollection<string, FileSchemaColumn>(fsc => fsc.Name);

            //this._FileSets = new EntitySet<FileSet>(new Action<FileSet>(fs => fs.FileSchema = this), new Action<FileSet>(fs => fs.FileSchema = null));
            this._FileSchemaColumns = new EntitySet<FileSchemaColumn>(this.OnFileSchemaColumnAdded, new Action<FileSchemaColumn>(c => c.FileSchema = null));
        }

        private void OnFileSchemaColumnAdded(FileSchemaColumn column)
        {
            column.FileSchema = this;
            this._indexedColumns.Add(column);
        }

        [OnDeserializing]
        internal void OnDeserializing(StreamingContext context)
        {
            this.Initialize();
        }

        #region Public Methods

        public void AddIdColumn(string columnName)
        {
            this.AddIdColumn(columnName, null);
        }

        public void AddIdColumn(string columnName, string label)
        {
            ExceptionHelper.ThrowIf<InvalidOperationException>(this.IdentifierColumn != null, "An Id column has already been added to this schema");

            IdFileSchemaColumn idColumn = new IdFileSchemaColumn();
            this.AddColumn(idColumn, columnName, label);
        }

        public void AddGenderColumn(string columnName, string maleValue, string femaleValue)
        {
            this.AddGenderColumn(columnName, null, maleValue, femaleValue);
        }

        public void AddGenderColumn(string columnName, string label, string maleValue, string femaleValue)
        {
            ExceptionHelper.ThrowIf<InvalidOperationException>(this.GenderColumn != null, "A gender column has already been added to this schema");

            FileSchemaColumn genderColumn = new GenderFileSchemaColumn(maleValue, femaleValue);
            this.AddColumn(genderColumn, columnName, label);
        }

        public ScaleFileSchemaColumn AddScaleColumn(string columnName)
        {
            return this.AddScaleColumn(columnName, null);
        }

        public ScaleFileSchemaColumn AddScaleColumn(string columnName, string label)
        {
            ScaleFileSchemaColumn column = new ScaleFileSchemaColumn();
            this.AddColumn(column, columnName, label);

            return column;
        }

        public NominalFileSchemaColumn AddNominalColumn(string columnName)
        {
            return this.AddNominalColumn(columnName, null);
        }

        public NominalFileSchemaColumn AddNominalColumn(string columnName, string label)
        {
            NominalFileSchemaColumn column = new NominalFileSchemaColumn();
            this.AddColumn(column, columnName, label);

            return column;
        }

        public OrdinalFileSchemaColumn AddOrdinalColumn(string columnName)
        {
            return this.AddOrdinalColumn(columnName, null);
        }

        public OrdinalFileSchemaColumn AddOrdinalColumn(string columnName, string label)
        {
            OrdinalFileSchemaColumn column = new OrdinalFileSchemaColumn();
            this.AddColumn(column, columnName, label);

            return column;
        }

        public virtual int GetColumnPhenotypeIndex(string columnName)
        {
            var col = this[columnName];
            ExceptionHelper.ThrowIf<ArgumentException>(!col.IsValidPhenotypeColumn, String.Format("Column {0} is not a valid phenotype column", columnName));

            return this.GetMappedColumnIndex(this.PhenotypeColumns.Cast<FileSchemaColumn>(), col.Idx);
        }

        public virtual int GetColumnCategoryIndex(string columnName)
        {
            var col = this[columnName];
            ExceptionHelper.ThrowIf<ArgumentException>(!(col is NominalFileSchemaColumn), String.Format("Column {0} is not a categorical column", columnName));

            return this.GetMappedColumnIndex(this.FileSchemaColumns.OfType<NominalFileSchemaColumn>().Cast<FileSchemaColumn>(), col.Idx);
        }

        private int GetMappedColumnIndex(IEnumerable<FileSchemaColumn> columns, int columnIndex)
        {
            int mappedIndex = 0;
            var phenotypeCol = columns.First(c =>
            {
                bool found = c.Idx == columnIndex;
                if (!found)
                    mappedIndex++;

                return found;
            });

            return mappedIndex;
        }

        /// <summary>
        /// Loads a schema definition from an xml file
        /// </summary>
        /// <param name="fileName">The file containing the schema definition in xml format</param>
        public static FileSchema LoadFromXml(string fileName)
        {
            var doc = XElement.Load(fileName);
            FileSchema fs = new FileSchema();
            fs.Load(doc);
            return fs;
        }

        /// <summary>
        /// Saves this file schema definition to an xml file
        /// </summary>
        /// <param name="fileName">The destination file to write to</param>
        public void Save(string fileName)
        {
            throw new NotImplementedException();
        }

        public void Validate(TextDataTableReader rawPhenotypeDataReader, ValidationMessageCollection validationMessages)
        {
            //check input arguments
            rawPhenotypeDataReader.ThrowIfNull();
            validationMessages.ThrowIfNull();

            //check id and gender columns exist
            this.IdentifierColumn.ThrowIfNull<InvalidOperationException>("Cannot validate input - not identifier column has been added");
            this.GenderColumn.ThrowIfNull<InvalidOperationException>("Cannot validate input - no gender column has been added");

            //check the presence of each column in the data - it is an error if either the id or gender column is missing
            foreach (string colName in this.FileSchemaColumns.Select(c => c.Name))
            {
                if (!rawPhenotypeDataReader.ColumnExists(colName))
                {
                    if (colName == this.IdentifierColumn.Name)
                        validationMessages.AddErrorMessage(String.Format("Identifier column '{0}' not found in data", colName));
                    else if (colName == this.GenderColumn.Name)
                        validationMessages.AddErrorMessage(String.Format("Gender column '{0}' not found in data", colName));
                    else
                        validationMessages.AddWarningMessage(String.Format("Column '{0}' not found in data", colName));
                }
            }

            //only continue validation if no errors have occured (i.e. the id and gender columns are present)
            if (validationMessages.ErrorCount == 0)
            {
                //iterate through the rows of the phenotype data and use the columns to validate each data element
                foreach (var dataRow in rawPhenotypeDataReader)
                {
                    foreach (var column in this.FileSchemaColumns)
                    {
                        column.Validate(dataRow[column.Name], validationMessages);
                    }
                }
            }
        }

        #endregion

        #region Protected Methods

        protected void Load(XElement doc)
        {
            doc.ThrowIfNull("doc");

            var columns = from node in doc.Descendants("Column")
                          orderby int.Parse(node.Attribute("position").Value)
                          let labelAtt = node.Attribute("label")
                          let idAtt = node.Attribute("id")
                          let genderAtt = node.Attribute("gender")
                          let genderValues = genderAtt == null ? null : genderAtt.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
                          select new
                          {
                              Name = node.Attribute("name").Value,
                              Type = (ColumnDistribution)Enum.Parse(typeof(ColumnDistribution), node.Attribute("type").Value),
                              Label = labelAtt == null ? null : labelAtt.Value,
                              IsIdentifier = idAtt == null ? false : (bool)idAtt,
                              MaleValue = genderValues == null ? null : genderValues[0],
                              FemaleValue = genderValues == null ? null : genderValues[1],
                              Values = from valNode in node.Descendants("Value")
                                       let missingAtt = valNode.Attribute("missing")
                                       let valLabelAtt = valNode.Attribute("label")
                                       select new
                                       {
                                           Value = (string)valNode,
                                           Missing = missingAtt == null ? false : (bool)missingAtt,
                                           Label = valLabelAtt == null ? null : valLabelAtt.Value
                                       }
                          };

            foreach (var colInfo in columns)
            {
                if (colInfo.IsIdentifier)
                {
                    this.AddIdColumn(colInfo.Name, colInfo.Label);
                }
                else if (colInfo.MaleValue != null && colInfo.FemaleValue != null)
                {
                    this.AddGenderColumn(colInfo.Name, colInfo.Label, colInfo.MaleValue, colInfo.FemaleValue);
                }
                else
                {
                    switch (colInfo.Type)
                    {
                        case ColumnDistribution.Scale:
                            {
                                var scaleCol = this.AddScaleColumn(colInfo.Name, colInfo.Label);
                                foreach (string missingValue in colInfo.Values.Where(v => v.Missing).Select(mv => mv.Value))
                                {
                                    scaleCol.AddMissingValue(missingValue);
                                }
                                break;
                            }
                        case ColumnDistribution.Nominal:
                        case ColumnDistribution.Ordinal:
                            {
                                NominalFileSchemaColumn col = colInfo.Type == ColumnDistribution.Nominal
                                    ? this.AddNominalColumn(colInfo.Name, colInfo.Label)
                                    : this.AddOrdinalColumn(colInfo.Name, colInfo.Label);

                                //add values to column
                                foreach (var val in colInfo.Values)
                                {
                                    if (val.Missing)
                                        col.AddMisingValue(val.Value);
                                    else
                                        col.AddValue(val.Value, val.Label);
                                }
                                break;
                            }

                        default:
                            //should not happen...
                            break;
                    }
                }
            }
        }

        #endregion

        #region Private Methods

        private bool ColumnExists(string columnName)
        {
            return this.Columns.Any(c => c.Name == columnName);
        }

        private void AddColumn(FileSchemaColumn column, string name, string label)
        {
            Debug.Assert(column != null);

            if (name.IsEmptyOrWhitespace())
                throw new ArgumentException("Column name cannot be empty");

            if (this.ColumnExists(name))
                throw new ArgumentException(String.Format("Column '{0}' already exists in this schema", name));

            column.Name = name;
            column.Label = label;
            column.Idx = (short)this.FileSchemaColumns.Count;
            this.FileSchemaColumns.Add(column);
        }

        #endregion

        #region Properties

        public FileSchemaColumn this[int index]
        {
            get { return this.FileSchemaColumns[index]; }
        }

        public virtual FileSchemaColumn this[string columnName]
        {
            get
            {
                columnName.ThrowIfNull("columnName");

                if (this._indexedColumns.Count == 0)
                {
                    this._indexedColumns.AddRange(this.Columns);
                }

                ExceptionHelper.ThrowIf<ArgumentException>(!this._indexedColumns.ContainsKey(columnName), String.Format("Column {0} not found", columnName ?? "<null>"));
                return this._indexedColumns[columnName];
            }
        }

        public FileSchemaColumn IdentifierColumn
        {
            get
            {
                return this.FileSchemaColumns.OfType<IdFileSchemaColumn>().SingleOrDefault();
            }
        }

        public FileSchemaColumn GenderColumn
        {
            get
            {
                return this.FileSchemaColumns.OfType<GenderFileSchemaColumn>().SingleOrDefault();
            }
        }

        public int Count
        {
            get { return this.FileSchemaColumns.Count; }
        }

        public virtual IEnumerable<NominalFileSchemaColumn> BinaryColumns
        {
            get
            {
                return this.FileSchemaColumns.OfType<NominalFileSchemaColumn>().Where(c => c.IsBinary);
            }
        }

        public virtual IEnumerable<NominalFileSchemaColumn> CategoricalColumns
        {
            get
            {
                return this.FileSchemaColumns.OfType<NominalFileSchemaColumn>().Where(c => !c.IsBinary);
            }
        }

        public virtual IEnumerable<ScaleFileSchemaColumn> ContinuousColumns
        {
            get
            {
                return this.FileSchemaColumns.OfType<ScaleFileSchemaColumn>();
            }
        }

        public IEnumerable<FileSchemaColumn> Columns
        {
            get { return this.FileSchemaColumns; }
        }

        public virtual IEnumerable<FileSchemaColumn> PhenotypeColumns
        {
            get { return this.Columns.Where(c => c.IsValidPhenotypeColumn); }
        }

        #endregion
    }
}
